// --------------------------------------------------------------------------------------------------------------------
// <copyright file="ApplyDefaultValuesExtension.cs" company="CPOL">
//   Eugene Sadovoi
// </copyright>
// <summary>
//   Defines the ApplyDefaultValuesExtension type.
// </summary>
// --------------------------------------------------------------------------------------------------------------------
namespace BatchID3.Extensions
{
    using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Linq.Expressions;
    using System.Reflection;

    public static class ApplyDefaultValuesExtension
    {
        // Dictionary to hold type initialization methods' cache 
        private static readonly Dictionary<Type, Action<object>> TypesInitializers = new Dictionary<Type, Action<object>>();

        // Implements precompiled setters with embedded constant values from DefaultValueAttributes
        public static void ApplyDefaultValues(this object obj)
        {
            Action<object> setter = null;

            // Attempt to get it from cache
            if (!TypesInitializers.TryGetValue(obj.GetType(), out setter))
            {
                // If no initializers are added do nothing
                setter = (o) => { };

                // Iterate through each property
                ParameterExpression objectTypeParam = Expression.Parameter(typeof(object), "this");
                foreach (PropertyInfo prop in obj.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance))
                {
                    Expression defaultValueAttribute;

                    // Skip read only properties
                    if (!prop.CanWrite)
                    {
                        continue;
                    }

                    // There are no more then one attribute of this type
                    var attr = prop.GetCustomAttributes(typeof(DefaultValueAttribute), false) as DefaultValueAttribute[];

                    // Skip properties with no DefaultValueAttribute
                    if (attr == null || attr.Length == 0 || attr[0] == null)
                    {
                        continue;
                    }

                    // Build the Lambda expression
#if DEBUG

                    // Make sure types do match
                    try
                    {
                        defaultValueAttribute = Expression.Convert(
                            Expression.Constant(attr[0].Value), prop.PropertyType);
                    }
                    catch (InvalidOperationException e)
                    {
                        var error =
                            string.Format(
                                "Type of DefaultValueAttribute({3}{0}{3}) does not match type of property {1}.{2}", 
                                attr[0].Value, 
                                obj.GetType().Name, 
                                prop.Name, 
                                (attr[0].Value is string) ? "\"" : string.Empty);

                        throw new InvalidOperationException(error, e);
                    }

#else
					defaultValueAttribute = Expression.Convert(Expression.Constant(attr[0].Value), prop.PropertyType);
#endif
                    var setExpression = Expression.Call(
                        Expression.TypeAs(objectTypeParam, obj.GetType()), prop.GetSetMethod(), defaultValueAttribute);
                    var setLambda = Expression.Lambda<Action<object>>(setExpression, objectTypeParam);

                    // Add this action to multicast delegate
                    setter += setLambda.Compile();
                }

                // Save in the type cache
                TypesInitializers.Add(obj.GetType(), setter);
            }

            // Initialize member properties
            setter(obj);
        }

        // Implements cache of ResetValue delegates
        public static void ResetDefaultValues(this object obj)
        {
            Action<object> setter = null;

            // Attempt to get it from cache
            if (!TypesInitializers.TryGetValue(obj.GetType(), out setter))
            {
                // Init delegate with empty body,
                // If no initializers are added do nothing
                setter = (o) => { };

                // Go throu each property and compile Reset delegates
                foreach (PropertyDescriptor prop in TypeDescriptor.GetProperties(obj))
                {
                    // Add only these which values can be reset
                    if (prop.CanResetValue(obj))
                    {
                        setter += prop.ResetValue;
                    }
                }

                // Save in the type cache
                TypesInitializers.Add(obj.GetType(), setter);
            }

            // Initialize member properties
            setter(obj);
        }
    }
}